package ex3.visitors;

import common.exceptions.SemanticError;
import ic.ast.Visitor;
import ic.ast.decl.*;
import ic.ast.expr.*;
import ic.ast.stmt.*;
import ic.symbols.*;

import java.util.LinkedList;
import java.util.List;

/**
 * Created with IntelliJ IDEA.
 * User: Roi
 * Date: 12/6/13
 *
 * A visitor which builds the program's symbol tables
 */
public class SymbolTableVisitor implements Visitor {

    @Override
    public Object visit(Program program) throws Exception {
        GlobalSymbolTable globalSymbolTable = new GlobalSymbolTable(null);
        program.setEnclosingScope(globalSymbolTable);

        // Insert all classes to the symbol table
        for (DeclClass declClass : program.getClasses()) {
            try {
                globalSymbolTable.insert(declClass);
            } catch (SemanticError semantic) {
                throw new SemanticError(declClass.getLine(), semantic.getMessage());
            }
        }

        // Recursively visit all child classes
        for (DeclClass declClass : program.getClasses()) {
            String superClass = declClass.getSuperClassName();
            if (superClass != null) {
                ClassSymbol superClassSymbol = globalSymbolTable.getClassSymbol(superClass);
                if (superClassSymbol == null) {
                    throw new SemanticError(declClass.getLine(),
                            "Class " + declClass.getName()
                                    + " cannot extend " + superClass
                                    + ", since it's not yet defined");
                }
            }
            declClass.setEnclosingScope(globalSymbolTable);
            declClass.accept(this);
        }

        // Correct class symbol table hierarchy (set parent)
        List<SymbolTable> toBeRemoved = new LinkedList<SymbolTable>();
        for (DeclClass classDecl : program.getClasses()) {
            if (classDecl.getSuperClassName() != null) {
                SymbolTable parent = globalSymbolTable.getSymbolTable(classDecl.getSuperClassName());
                SymbolTable child = globalSymbolTable.getSymbolTable(classDecl.getName());
                if (parent != null && child != null) {
                    parent.insertChildSymbolTable(child);
                    child.setParent(parent);
                    classDecl.setEnclosingScope(parent);
                    toBeRemoved.add(child);
                }
            }
        }
        for (SymbolTable child : toBeRemoved) {
            globalSymbolTable.removeChildTable(child.getName());
        }

        return globalSymbolTable;
    }

    @Override
    public Object visit(DeclClass icClass) throws Exception {
        ClassSymbolTable symbols = new ClassSymbolTable(icClass.getEnclosingScope(), icClass.getName());

        for (DeclField declField : icClass.getFields()) {
            try {
                symbols.insert(declField);
                declField.setEnclosingScope(symbols);
            } catch (SemanticError semantic) {
                if(semantic.getLine() == -1)
                    throw new SemanticError(declField.getLine(), semantic.getMessage());
                else
                    throw semantic;
            }
        }

        for (DeclMethod declMethod : icClass.getMethods()) {
            try {
                symbols.insert(declMethod);
                declMethod.setEnclosingScope(symbols);
                declMethod.accept(this);
            } catch (SemanticError semantic) {
                if(semantic.getLine() == -1)
                    throw new SemanticError(declMethod.getLine(), semantic.getMessage());
                else
                    throw semantic;
            }
        }

        icClass.getEnclosingScope().insertChildSymbolTable(symbols);
        return null;
    }

    @Override
    public Object visit(DeclField field) throws Exception {
        field.getType().setEnclosingScope(field.getEnclosingScope());
        field.getType().accept(this);
        return null;
    }

    @Override
    public Object visit(DeclVirtualMethod method) throws Exception {
        visitMethod(method, false);
        return null;
    }

    @Override
    public Object visit(DeclStaticMethod method) throws Exception {
        visitMethod(method, true);
        return null;
    }

    @Override
    public Object visit(DeclLibraryMethod method) throws Exception {
        visitMethod(method, true);
        return null;
    }

    @Override
    public Object visit(Parameter formal) throws Exception {
        formal.getType().setEnclosingScope(formal.getEnclosingScope());
        formal.getType().accept(this);
        return null;
    }

    @Override
    public Object visit(PrimitiveType type) {
        return null;
    }

    @Override
    public Object visit(ClassType newClass) {
        return null;
    }

    @Override
    public Object visit(StmtAssignment assignment) throws Exception {
        assignment.getAssignment().setEnclosingScope(assignment.getEnclosingScope());
        assignment.getAssignment().accept(this);
        assignment.getVariable().setEnclosingScope(assignment.getEnclosingScope());
        assignment.getVariable().accept(this);
        return null;
    }

    @Override
    public Object visit(StmtCall callStatement) throws Exception {
        callStatement.getCall().setEnclosingScope(callStatement.getEnclosingScope());
        callStatement.getCall().accept(this);
        return null;
    }

    @Override
    public Object visit(StmtReturn returnStatement) throws Exception {
        if (returnStatement.getValue() == null)
            return null;

        returnStatement.getValue().setEnclosingScope(returnStatement.getEnclosingScope());
        returnStatement.getValue().accept(this);
        return null;
    }

    @Override
    public Object visit(StmtIf ifStatement) throws Exception {
        ifStatement.getCondition().setEnclosingScope(ifStatement.getEnclosingScope());
        ifStatement.getCondition().accept(this);
        ifStatement.getOperation().setEnclosingScope(ifStatement.getEnclosingScope());
        ifStatement.getOperation().accept(this);

        if (ifStatement.hasElse()) {
            ifStatement.getElseOperation().setEnclosingScope(ifStatement.getEnclosingScope());
            ifStatement.getElseOperation().accept(this);
        }

        return null;
    }

    @Override
    public Object visit(StmtWhile whileStatement) throws Exception {
        whileStatement.getCondition().setEnclosingScope(whileStatement.getEnclosingScope());
        whileStatement.getCondition().accept(this);
        whileStatement.getOperation().setEnclosingScope(whileStatement.getEnclosingScope());
        whileStatement.getOperation().accept(this);
        return null;
    }

    @Override
    public Object visit(StmtBreak breakStatement) {
        return null;
    }

    @Override
    public Object visit(StmtContinue continueStatement) {
        return null;
    }

    @Override
    public Object visit(StmtBlock statementsBlock) throws Exception {
        BlockSymbolTable symbols = new BlockSymbolTable(statementsBlock.getEnclosingScope());
        for (Statement stmt : statementsBlock.getStatements()) {
            stmt.setEnclosingScope(symbols);
            stmt.accept(this);
        }
        statementsBlock.getEnclosingScope().insertChildSymbolTable(symbols);
        return null;
    }

    @Override
    public Object visit(LocalVariable localVariable) throws Exception {
        try {
            ((BlockSymbolTable) localVariable.getEnclosingScope()).insert(localVariable);
        } catch (SemanticError semantic) {
            throw new SemanticError(localVariable.getLine(), semantic.getMessage());
        }
        localVariable.getType().setEnclosingScope(localVariable.getEnclosingScope());
        localVariable.getType().accept(this);
        if (localVariable.getInitialValue() != null) {
            localVariable.getInitialValue().setEnclosingScope(localVariable.getEnclosingScope());
            localVariable.getInitialValue().accept(this);
        }
        return null;
    }

    @Override
    public Object visit(RefVariable location) {
        return null;
    }

    @Override
    public Object visit(RefField location) throws Exception {
        if (location.getObject() == null) return null;
        location.getObject().setEnclosingScope(location.getEnclosingScope());
        location.getObject().accept(this);
        return null;
    }

    @Override
    public Object visit(RefArrayElement location) throws Exception {
        location.getArray().setEnclosingScope(location.getEnclosingScope());
        location.getArray().accept(this);
        location.getIndex().setEnclosingScope(location.getEnclosingScope());
        location.getIndex().accept(this);
        return null;
    }

    @Override
    public Object visit(StaticCall call) throws Exception {
        for (Expression expr : call.getArguments()) {
            expr.setEnclosingScope(call.getEnclosingScope());
            expr.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(VirtualCall call) throws Exception {
        if(call.getObject() != null){
            call.getObject().setEnclosingScope(call.getEnclosingScope());
            call.getObject().accept(this);
        }
        for (Expression expr : call.getArguments()) {
            expr.setEnclosingScope(call.getEnclosingScope());
            expr.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(This thisExpression) {
        return null;
    }

    @Override
    public Object visit(NewInstance newClass) {
        return null;
    }

    @Override
    public Object visit(NewArray newArray) throws Exception {
        newArray.getSize().setEnclosingScope(newArray.getEnclosingScope());
        newArray.getSize().accept(this);
        newArray.getType().setEnclosingScope(newArray.getEnclosingScope());
        newArray.getType().accept(this);
        return null;
    }

    @Override
    public Object visit(Length length) throws Exception {
        length.getArray().setEnclosingScope(length.getEnclosingScope());
        length.getArray().accept(this);
        return null;
    }

    @Override
    public Object visit(Literal literal) {
        return null;
    }

    @Override
    public Object visit(UnaryOp unaryOp) throws Exception {
        unaryOp.getOperand().setEnclosingScope(unaryOp.getEnclosingScope());
        unaryOp.getOperand().accept(this);
        return null;
    }

    @Override
    public Object visit(BinaryOp binaryOp) throws Exception {
        binaryOp.getFirstOperand().setEnclosingScope(binaryOp.getEnclosingScope());
        binaryOp.getFirstOperand().accept(this);
        binaryOp.getSecondOperand().setEnclosingScope(binaryOp.getEnclosingScope());
        binaryOp.getSecondOperand().accept(this);
        return null;
    }

    @Override
    public Object visit(GlobalSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(ClassSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(BlockSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(MethodSymbolTable table) {
        return null;
    }

    @Override
    public Object visit(SymbolTable symbolTable) {
        return null;
    }

    private void visitMethod(DeclMethod method, boolean isStatic) throws Exception {
        MethodSymbolTable symbols = new MethodSymbolTable(method.getEnclosingScope(), method.getName(), isStatic);
        method.getEnclosingScope().insertChildSymbolTable(symbols);

        for (Parameter parameter : method.getFormals()) {
            try {
                symbols.insert(parameter);
            } catch (SemanticError semantic) {
                throw new SemanticError(parameter.getLine(), semantic.getMessage());
            }
            parameter.setEnclosingScope(symbols);
            parameter.accept(this);
        }

        for (Statement statement : method.getStatements()) {
            statement.setEnclosingScope(symbols);
            statement.accept(this);
        }
    }

}
